﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace HerbertDotNet.Visualizer
{
    public partial class HerbertLevelViewer : UserControl
    {
        [Category("Program")]
        public event EventHandler<RuntimeErrorEventArgs> RuntimeError;

        [Category("Program")]
        public event EventHandler<EventArgs> CurrentRunStateChanged;

        protected IList<HPoint> _bgPath;             // Background path to display.

        protected HerbertInterpreter _interpreter;   // Interpreter to run program.
        protected IEnumerator<HProgramRunState?>
            _progEnumerator;                         // Enumerator for program run states.
        protected List<HProgramRunState> _runStates; // All run states up to current point of execution.
        protected int _currentRunState;              // Index of current run state to display.

        protected SizeF _tileSize;                   // Size of each tile, in pixels.
        protected SizeF _tileHalfSize;               // Half of size of each tile, in pixels.
        protected float _drawOX;                     // X-position of origin for drawing tiles.
        protected float _drawOY;                     // Y-position of origin for drawing tiles.

        protected Image[] _robotImages;              // Images of robot.
        protected int _robotImageIndex;              // Current index of robot image to display.

        public HerbertLevelViewer()
        {
            InitializeComponent();

            this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer
                | ControlStyles.ResizeRedraw, true);

            // Create array of robot images.
            _robotImages = new Image[] { Properties.Resources.robot_0, Properties.Resources.robot_1,
                Properties.Resources.robot_2 };
            _robotImageIndex = 0;

            // Create interpreter for program.
            _interpreter = new HerbertInterpreter();
            _interpreter.ReportPressedButtons = true;
        }

        [Browsable(false)]
        public HProgramRunState BackgroundPathLastState
        {
            get;
            set;
        }

        [Browsable(false)]
        public IList<HPoint> BackgroundPath
        {
            get
            {
                return _bgPath;
            }
            set
            {
                _bgPath = value;

                this.BackgroundPathLastState = new HProgramRunState()
                {
                    Position = this.Level == null ? new HPoint() : this.Level.StartPosition
                };
            }
        }

        [Browsable(false)]
        public HProgramRunState CurrentRunState
        {
            get { return _runStates[_currentRunState]; }
        }

        [Browsable(false)]
        public List<HProgramRunState> RunStates
        {
            get { return _runStates; }
        }

        [Browsable(false)]
        public int CurrentRunStateIndex
        {
            get { return _currentRunState; }
        }

        [Browsable(false)]
        public bool RuntimeErrorOccurred
        {
            get;
            protected set;
        }

        [Browsable(false)]
        public bool ProgramFinished
        {
            get;
            protected set;
        }

        [Browsable(false)]
        public HerbertLevel Level
        {
            get
            {
                return _interpreter.Level;
            }
            set
            {
                _interpreter.Level = value;

                if (_interpreter.Level != null)
                {
                    CalculateMeasurements();

                    // Reset list of run states.
                    ResetRunStates();
                }

                // Redraw control.
                this.Refresh();
            }
        }

        [Browsable(false)]
        public HerbertProgram Program
        {
            get
            {
                return _interpreter.Program;
            }
            set
            {
                _interpreter.Program = value;

                if (_interpreter.Program != null)
                {
                    // Create enumerator for program run states.
                    _progEnumerator = _interpreter.GetProgramRunStates().GetEnumerator();
                }
                else
                {
                    _progEnumerator = null;
                }

                if (_interpreter.Level != null)
                {
                    // Reset list of run states.
                    ResetRunStates();
                }

                // Redraw control.
                this.Refresh();
            }
        }

        [Browsable(false)]
        public HerbertInterpreter Interpreter
        {
            get { return _interpreter; }
        }

        public void DrawLevelToBitmap(Bitmap bitmap, Rectangle targetBounds)
        {
            // Draw image of level to specified bitmap.
            using (var g = Graphics.FromImage(bitmap))
            {
                var gstate = g.Save();

                g.SetClip(targetBounds);
                DrawLevel(g);

                g.Restore(gstate);
            }
        }

        public int GetLastScore()
        {
            if (_interpreter.Program == null) return 0;

            return _interpreter.CalculateScore();
        }

        public bool MoveToPreviousState()
        {
            if (this.Program == null) return false;

            // Check if program is at start.
            if (_currentRunState == 0) return false;

            // Move to previous run state of program.
            _currentRunState--;

            CurrentRunStateChanged(this, new EventArgs());

            // Invalidate control.
            this.Invalidate();

            return true;
        }

        public bool MoveToNextState()
        {
            if (this.Program == null) return false;

            // Check if program has finished.
            if (_currentRunState == _runStates.Count - 1 && this.ProgramFinished) return false;

            // Move to to next run state of program.
            _currentRunState++;

            if (_currentRunState == _runStates.Count - 1)
            {
                try
                {
                    // Get next run state.
                    this.ProgramFinished = !_progEnumerator.MoveNext();
                }
                catch (HerbertRuntimeException exRuntime)
                {
                    this.ProgramFinished = true;
                    this.RuntimeErrorOccurred = true;

                    RuntimeError(this, new RuntimeErrorEventArgs(exRuntime));

                    return false;
                }

                // Add new run state to list, if program has not yet finished.
                if (!this.ProgramFinished) _runStates.Add(_progEnumerator.Current.Value);
            }

            CurrentRunStateChanged(this, new EventArgs());

            // Invalidate control.
            this.Invalidate();

            return true;
        }

        protected void ResetRunStates()
        {
            // Recreate list of run states and add start position/direction as first state.
            _runStates = new List<HProgramRunState>();
            _runStates.Add(new HProgramRunState(this.Program == null ? 0 : this.Program.StartAddress,
                this.Level.StartPosition, HDirection.Up, new bool[this.Level.Tiles.GetLength(0),
                    this.Level.Tiles.GetLength(1)]));
            _currentRunState = 0;

            // Set that no error has yet occurred.
            this.RuntimeErrorOccurred = false;

            if (_progEnumerator == null)
            {
                // Set that program has already finished.
                this.ProgramFinished = true;
            }
            else
            {
                // Get first actual run state of program.
                var getFirstRunStateDelegate = new Action(GetFirstRunState);
                getFirstRunStateDelegate.BeginInvoke(null, null);
            }
        }

        public void Reset()
        {
            // Reset program.
            this.Program = this.Program;
        }

        protected void GetFirstRunState()
        {
            try
            {
                // Get first run state and determine whether program has finished already.
                this.ProgramFinished = !_progEnumerator.MoveNext();
            }
            catch (HerbertRuntimeException)
            {
                this.ProgramFinished = true;
                //RuntimeError(this, new RuntimeErrorEventArgs(exRuntime));
            }

            // Add first run state to list.
            if (!this.ProgramFinished && _progEnumerator.Current != null)
                _runStates.Add(_progEnumerator.Current.Value);

            this.Invoke(new EventHandler<EventArgs>(CurrentRunStateChanged), this, new EventArgs());
        }

        protected void DrawLevel(Graphics g)
        {
            // Check that level exists.
            if (this.Level == null) return;

            // Clear background to white.
            g.Clear(Color.White);

            // Get current run state of program.
            HProgramRunState curRunState = _runStates[_currentRunState];

            // Get bounds of 2D array of tiles.
            int ub0 = this.Level.Tiles.GetUpperBound(0);
            int ub1 = this.Level.Tiles.GetUpperBound(1);

            // Perform some calculations used for drawing.
            float buttonRadius = 3;
            float buttonDiameter = buttonRadius * 2;

            // Draw each tile.
            Pen wallPen = new Pen(Color.Black, 3);
            Pen buttonPen = new Pen(Color.Black, 1);

            try
            {
                // Draw each tile in level.
                HerbertLevel.Tile curTile;
                float drawX = _drawOX;
                float drawY = _drawOY;

                for (int x = 0; x <= ub0; x++)
                {
                    for (int y = 0; y <= ub1; y++)
                    {
                        curTile = this.Level.Tiles[x, y];

                        // Draw dot in centre of rectangle.
                        g.FillRectangle(Brushes.Black, drawX, drawY, 2, 2);

                        // Draw any walls.
                        if ((curTile & HerbertLevel.Tile.WallTop) != 0)
                            g.DrawLine(wallPen, drawX + 1, drawY + 1, drawX + 1,
                                drawY - _tileHalfSize.Height);
                        if ((curTile & HerbertLevel.Tile.WallBottom) != 0)
                            g.DrawLine(wallPen, drawX + 1, drawY + 1, drawX + 1,
                                drawY + _tileHalfSize.Height);
                        if ((curTile & HerbertLevel.Tile.WallLeft) != 0)
                            g.DrawLine(wallPen, drawX + 1, drawY + 1, drawX - _tileHalfSize.Width,
                                drawY + 1);
                        if ((curTile & HerbertLevel.Tile.WallRight) != 0)
                            g.DrawLine(wallPen, drawX + 1, drawY + 1, drawX + _tileHalfSize.Width,
                                drawY + 1);

                        if (curTile == HerbertLevel.Tile.WhiteButton)
                        {
                            // Draw white button.
                            g.SmoothingMode = SmoothingMode.HighQuality;
                            g.DrawEllipse(buttonPen, drawX - buttonRadius + 1, drawY - buttonRadius + 1,
                                buttonDiameter, buttonDiameter);
                            g.FillEllipse(curRunState.PressedButtons[x, y] ? Brushes.Black
                                : Brushes.White, drawX - buttonRadius + 1, drawY - buttonRadius + 1,
                                buttonDiameter, buttonDiameter);
                            g.SmoothingMode = SmoothingMode.Default;
                        }

                        if (curTile == HerbertLevel.Tile.GrayButton)
                        {
                            // Draw gray button.
                            g.SmoothingMode = SmoothingMode.HighQuality;
                            g.DrawEllipse(buttonPen, drawX - buttonRadius + 1, drawY - buttonRadius + 1,
                                buttonDiameter, buttonDiameter);
                            g.FillEllipse(Brushes.Gray, drawX - buttonRadius + 1, drawY - buttonRadius + 1,
                                buttonDiameter, buttonDiameter);
                            g.SmoothingMode = SmoothingMode.Default;
                        }

                        drawY += _tileSize.Height;
                    }

                    drawX += _tileSize.Width;
                    drawY = _drawOY;
                }
            }
            finally
            {
                wallPen.Dispose();
            }
        }

        protected void CalculateMeasurements()
        {
            // Check that level exists.
            if (this.Level == null) return;

            // Perform some calculations used for drawing.
            float tileDimension = Math.Min((float)this.ClientSize.Width / (this.Level.Tiles.GetLength(0)
                + 1), (float)this.ClientSize.Height / (this.Level.Tiles.GetLength(1) + 1));

            _tileSize = new SizeF(tileDimension, tileDimension);
            _tileHalfSize = new SizeF(_tileSize.Width / 2.0f, _tileSize.Height / 2.0f);
            _drawOX = (this.ClientRectangle.Width / 2) - (_tileSize.Width
                * (this.Level.Tiles.GetLength(0) + 1) / 2) + _tileSize.Width;
            _drawOY = (this.ClientRectangle.Height / 2) - (_tileSize.Height
                * (this.Level.Tiles.GetLength(1) + 1) / 2) + _tileSize.Height;
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            // Check that level exists.
            if (this.Level == null) return;

            // Draw level to surface.
            DrawLevel(e.Graphics);

            // Get current run state of program.
            HProgramRunState curRunState = _runStates[_currentRunState];

            // Check if background path currently exists.
            if (this.BackgroundPath != null)
            {
                Pen pathPen = new Pen(Color.Red, 2);

                try
                {
                    // Draw background path of robot.
                    HPoint lastPoint = new HPoint(-1, -1);
                    PointF lastDrawPoint = new PointF(-1f, -1f);
                    PointF curDrawPoint;

                    foreach (HPoint point in _bgPath)
                    {
                        curDrawPoint = new PointF(_drawOX + point.X * _tileSize.Width + 1,
                            _drawOY + point.Y * _tileSize.Height + 1);

                        // Check that this is not first part.
                        if (lastDrawPoint.X != -1f)
                        {
                            // Check that point is no more than one tile away from last point.
                            if (Math.Abs(point.X - lastPoint.X) + Math.Abs(point.Y - lastPoint.Y) <= 1)
                                e.Graphics.DrawLine(pathPen, lastDrawPoint, curDrawPoint);
                        }

                        lastPoint = point;
                        lastDrawPoint = curDrawPoint;
                    }

                    // Draw indicator of last direction of robot on background path.
                    const float arrowWidthLengthRatio = 0.80f;

                    var arrowheadSizeX = _tileSize.Width * 0.25f;
                    var arrowheadSizeY = _tileSize.Height * 0.25f;
                    var arrowheadPoints = new SizeF[3];

                    switch(this.BackgroundPathLastState.Direction)
                    {
                        case HDirection.Up:
                            arrowheadPoints[0] = new SizeF(0, -arrowheadSizeY);
                            arrowheadPoints[1] = new SizeF(-arrowheadSizeX * arrowWidthLengthRatio, 
                                arrowheadSizeY);
                            arrowheadPoints[2] = new SizeF(arrowheadSizeX * arrowWidthLengthRatio,
                                arrowheadSizeY);

                            break;
                        case HDirection.Down:
                            arrowheadPoints[0] = new SizeF(0, arrowheadSizeY);
                            arrowheadPoints[1] = new SizeF(-arrowheadSizeX * arrowWidthLengthRatio, 
                                -arrowheadSizeY);
                            arrowheadPoints[2] = new SizeF(arrowheadSizeX * arrowWidthLengthRatio, 
                                -arrowheadSizeY);

                            break;
                        case HDirection.Left:
                            arrowheadPoints[0] = new SizeF(-arrowheadSizeX, 0);
                            arrowheadPoints[1] = new SizeF(arrowheadSizeX, -arrowheadSizeY
                                * arrowWidthLengthRatio);
                            arrowheadPoints[2] = new SizeF(arrowheadSizeX, arrowheadSizeY
                                * arrowWidthLengthRatio);

                            break;
                        case HDirection.Right:
                            arrowheadPoints[0] = new SizeF(arrowheadSizeX, 0);
                            arrowheadPoints[1] = new SizeF(-arrowheadSizeX, -arrowheadSizeY
                                * arrowWidthLengthRatio);
                            arrowheadPoints[2] = new SizeF(-arrowheadSizeX, arrowheadSizeY
                                * arrowWidthLengthRatio);

                            break;
                    }

                    curDrawPoint = new PointF(_drawOX + this.BackgroundPathLastState.Position.X
                        * _tileSize.Width + 1, _drawOY + this.BackgroundPathLastState.Position.Y
                        * _tileSize.Height + 1);

                    //e.Graphics.DrawLine(pathPen, curDrawPoint, curDrawPoint + arrowSize);
                    e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
                    e.Graphics.FillPolygon(Brushes.DarkRed, (from point in arrowheadPoints
                                                             select curDrawPoint + point).ToArray());
                    e.Graphics.SmoothingMode = SmoothingMode.Default;
                }
                finally
                {
                    pathPen.Dispose();
                }
            }

            // Check if list of run states currently exists.
            if (_runStates != null)
            {
                Pen pathPen = new Pen(Color.Blue, 2);

                try
                {
                    // Draw path of robot up to current point of execution.
                    PointF lastDrawPoint = new PointF(-1f, -1f);
                    PointF curDrawPoint;
                    HProgramRunState runState;

                    for (int i = 0; i <= _currentRunState; i++)
                    {
                        runState = _runStates[i];
                        curDrawPoint = new PointF(_drawOX + runState.Position.X * _tileSize.Width + 1,
                            _drawOY + runState.Position.Y * _tileSize.Height + 1);

                        if (lastDrawPoint.X != -1f)
                            e.Graphics.DrawLine(pathPen, lastDrawPoint, curDrawPoint);

                        lastDrawPoint = curDrawPoint;
                    }
                }
                finally
                {
                    pathPen.Dispose();
                }
            }

            // Draw robot at current position and facing current direction.
            HPoint robotPosition = curRunState.Position;
            HDirection robotDirection = curRunState.Direction;

            Image robotImage = _robotImages[_robotImageIndex];
            Point robotDrawPosition = Point.Round(new PointF(_drawOX + (robotPosition.X * _tileSize.Width),
                _drawOY + (robotPosition.Y * _tileSize.Height)) - _tileHalfSize);
            float rotationAmount = 0f;

            switch (robotDirection)
            {
                case HDirection.Up:
                    rotationAmount = 0f;
                    break;
                case HDirection.Right:
                    rotationAmount = 90f;
                    break;
                case HDirection.Down:
                    rotationAmount = 180f;
                    break;
                case HDirection.Left:
                    rotationAmount = 270f;
                    break;
            }

            try
            {
                e.Graphics.TranslateTransform(robotDrawPosition.X + _tileHalfSize.Width,
                    robotDrawPosition.Y + _tileHalfSize.Height);
                e.Graphics.RotateTransform(rotationAmount);
                e.Graphics.DrawImage(robotImage, new RectangleF(-_tileHalfSize.Width, -_tileHalfSize.Height,
                    _tileSize.Width, _tileSize.Height));
            }
            finally
            {
                e.Graphics.ResetTransform();
            }
        }

        protected override void OnClientSizeChanged(EventArgs e)
        {
            base.OnClientSizeChanged(e);

            CalculateMeasurements();
        }
    }

    public class RuntimeErrorEventArgs : EventArgs
    {
        public RuntimeErrorEventArgs(HerbertRuntimeException error)
        {
            this.Error = error;
        }

        public HerbertRuntimeException Error
        {
            get;
            set;
        }
    }
}
